﻿'
' Will Strohl (will.strohl@gmail.com)
' http://www.willstrohl.com
'
'Copyright (c) 2011-2012, Will Strohl
'All rights reserved.
'
'Redistribution and use in source and binary forms, with or without modification, are 
'permitted provided that the following conditions are met:
'
'Redistributions of source code must retain the above copyright notice, this list of 
'conditions and the following disclaimer.
'
'Redistributions in binary form must reproduce the above copyright notice, this list 
'of conditions and the following disclaimer in the documentation and/or other 
'materials provided with the distribution.
'
'Neither the name of Will Strohl, Open Graph for DotNetNuke, nor the names of its contributors may be 
'used to endorse or promote products derived from this software without specific prior 
'written permission.
'
'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
'EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
'OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
'SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
'INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
'TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
'BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
'CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
'ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
'DAMAGE.
'

Imports DotNetNuke.Services.FileSystem
Imports System.Globalization
Imports System.IO
Imports System.Linq
Imports System.Text
Imports System.Text.RegularExpressions
Imports System.Xml.Linq
Imports System.Web

Namespace WillStrohl.Modules.OpenGraph

    Public NotInheritable Class OpenGraphWriter
        Implements IHttpModule

#Region " Private Members "

        Private mApplication As HttpApplication
        Private Const INVALID_EXTENSION_MATCH_PATTERN As String = "\.(swf|jpg|jpeg|jpe|gif|bmp|png|doc|docx|xls|xlsx|ppt|pptx|pdf|txt|xml|xsl|xsd|css|zip|template|htmtemplate|ico|avi|mpg|mpeg|mp3|wmv|mov|wav|js|axd|htm|html)$"
        Private Const INVALID_REQUEST_PATCH_MATCH_PATTERN As String = "(ctl=Edit|mid=\d+|popUp=true|ctl/Edit|mid/\d+|/Admin/|/Host/|/DesktopModules/)"
        'Private Const REQUEST_POST As String = "POST"
        'Private Const REQUEST_CONTENTTYPE_AJAX As String = "application/json"
        Private Const HEADER_AJAX_KEY As String = "X-MicrosoftAjax"
        Private Const HEADER_AJAX_VALUE As String = "Delta=true"

#End Region

#Region " Constructors "

        Public Sub New()
        End Sub

#End Region

#Region " Event Handlers "

        Public Sub Application_BeginRequest(ByVal sender As [Object], ByVal e As EventArgs)

            ' *** Enable the values below for debugging/dev only ***
            'Dim val01 As String = mApplication.Context.Request.UrlReferrer.AbsolutePath
            'Dim val02 As String = mApplication.Context.Request.UrlReferrer.AbsoluteUri
            'Dim val03 As String = mApplication.Context.Request.UrlReferrer.Host
            'Dim val04 As String = mApplication.Context.Request.UrlReferrer.LocalPath
            'Dim val05 As String = mApplication.Context.Request.UrlReferrer.OriginalString
            'Dim val06 As String = mApplication.Context.Request.UrlReferrer.PathAndQuery
            'Dim val07 As String = mApplication.Context.Request.UrlReferrer.Query
            'Dim val08 As String = mApplication.Context.Request.RawUrl
            'Dim val09 As String = mApplication.Context.Request.Url.AbsolutePath
            'Dim val10 As String = mApplication.Context.Request.Url.AbsoluteUri
            'Dim val11 As String = mApplication.Context.Request.Url.Host
            'Dim val12 As String = mApplication.Context.Request.Url.LocalPath
            'Dim val13 As String = mApplication.Context.Request.Url.OriginalString
            'Dim val14 As String = mApplication.Context.Request.Url.PathAndQuery
            'Dim val15 As String = mApplication.Context.Request.Url.Query

            ' get a reference to detect if the request is from DNN AJAX
            Dim strAjaxHeader As String = String.Empty
            If Not mApplication.Request.Headers(HEADER_AJAX_KEY) Is Nothing Then
                strAjaxHeader = mApplication.Request.Headers(HEADER_AJAX_KEY).ToString()
            End If

            ' only proceed if parsing for applicable web pages (ignore postbacks, reloads, editing pages, admin, host, and AJAX)
            If Not Regex.IsMatch(mApplication.Context.Request.Url.LocalPath, INVALID_EXTENSION_MATCH_PATTERN, RegexOptions.IgnoreCase) _
                AndAlso Not Regex.IsMatch(mApplication.Context.Request.Url.OriginalString, INVALID_REQUEST_PATCH_MATCH_PATTERN, RegexOptions.IgnoreCase) _
                AndAlso Not String.Equals(strAjaxHeader, HEADER_AJAX_VALUE) Then

                ' Create a new filter
                Dim mStreamFilter As New OpenGraphStream(mApplication.Response.Filter, mApplication)

                ' Insert it onto the page
                mApplication.Response.Filter = mStreamFilter

            End If

        End Sub

#End Region

#Region " Implementations "

        Public Sub Init(application As HttpApplication) Implements IHttpModule.Init

            ' Wire up beginrequest
            AddHandler application.BeginRequest, AddressOf Me.Application_BeginRequest

            ' Save the application
            mApplication = application

        End Sub

        Public Sub Dispose() Implements IHttpModule.Dispose

        End Sub

#End Region

        Public NotInheritable Class OpenGraphStream
            Inherits Stream

#Region " Constants "

            Private Const HEAD_END_TAG As String = "</head>"
            Private Const CONFIGURATION_PATH As String = "/DesktopModules/WillStrohl.OpenGraph/Config/ogConfig.xml.resources"
            Private Const SITE_CONFIGURATION_PATH As String = "/DesktopModules/WillStrohl.OpenGraph/Config/ogSettings-{0}-{1}.xml.resources"
            Private Const MATCH_TAG_PATTERN As String = "<meta\sproperty=""{0}""\scontent=""[\w\.\s:|]+""\s?/>"
            Private Const ERROR_HEAD_TAG As String = "<!-- The Open Graph Protocol tag module threw an error --></head>"
            Private Const IMAGE_MATCH As String = "^FileID=\d+$"
            Private Const IMAGE_ID_MATCH As String = "\d+"
            Private Const HTTP_PREFIX As String = "http://"

            Private Const TITLE_ELEMENT As String = "Title"
            Private Const TITLE_OG_TAG_FORMAT As String = "<meta property=""og:title"" content=""{0}""/>"
            Private Const URL_ELEMENT As String = "Url"
            Private Const URL_OG_TAG_FORMAT As String = "<meta property=""og:url"" content=""{0}""/>"
            Private Const IMAGE_ELEMENT As String = "Image"
            Private Const IMAGE_OG_TAG_FORMAT As String = "<meta property=""og:image"" content=""{0}""/>"
            Private Const DESCRIPTION_ELEMENT As String = "Description"
            Private Const DESCRIPTION_OG_TAG_FORMAT As String = "<meta property=""og:description"" content=""{0}""/>"
            Private Const LOCALE_ELEMENT As String = "Locale"
            Private Const LOCALE_OG_TAG_FORMAT As String = "<meta property=""og:locale"" content=""{0}""/>"
            Private Const SITENAME_ELEMENT As String = "SiteName"
            Private Const SITENAME_OG_TAG_FORMAT As String = "<meta property=""og:site_name"" content=""{0}""/>"
            Private Const TYPE_ELEMENT As String = "Type"
            Private Const TYPE_OG_TAG_FORMAT As String = "<meta property=""og:type"" content=""{0}""/>"
            Private Const FACEBOOKADMINS_ELEMENT As String = "FacebookAdmins"
            Private Const FACEBOOKADMINS_OG_TAG_FORMAT As String = "<meta property=""fb:admins"" content=""{0}""/>"
            Private Const FACEBOOKAPPID_ELEMENT As String = "FacebookAppId"
            Private Const FACEBOOKAPPID_OG_TAG_FORMAT As String = "<meta property=""fb:app_id"" content=""{0}""/>"

#End Region

#Region " Private Members "

            Private currentApp As HttpApplication

            '  The stream to the client
            Private moStream As Stream
            ' Used to track properties not supported by the client stream
            Private mlLength As Long
            Private mlPosition As Long
            ' An easy way to write a stream to the client
            Private mSR As StreamWriter

#End Region

#Region " Constructors "

            Public Sub New(stream As System.IO.Stream, ByVal CurrentApplication As HttpApplication)

                ' Save of the stream back to the client
                moStream = stream

                ' Create a streamwriter for later use
                mSR = New StreamWriter(moStream)

                currentApp = CurrentApplication

            End Sub

#End Region

#Region " Properties "

            Public Overrides ReadOnly Property CanRead() As Boolean
                Get
                    Return False
                End Get
            End Property

            Public Overrides ReadOnly Property CanSeek() As Boolean
                Get
                    Return True
                End Get
            End Property

            Public Overrides ReadOnly Property CanWrite() As Boolean
                Get
                    Return True
                End Get
            End Property

            Public Overrides ReadOnly Property Length() As Long
                Get
                    Return mlLength
                End Get
            End Property

            Public Overrides Property Position() As Long
                Get
                    Return mlPosition
                End Get
                Set(value As Long)
                    mlPosition = value
                End Set
            End Property

#End Region

#Region " Helper Methods "

            Public Overrides Function Read(ByVal buffer As [Byte](), ByVal offset As Integer, ByVal count As Integer) As Integer
                Throw New NotSupportedException()
            End Function

            Public Overrides Function Seek(ByVal offset As Long, ByVal direction As System.IO.SeekOrigin) As Long
                Return moStream.Seek(offset, direction)
            End Function

            Public Overrides Sub SetLength(ByVal length As Long)
                mlLength = length
            End Sub

            Public Overrides Sub Close()
                moStream.Close()
            End Sub

            Public Overrides Sub Flush()
                moStream.Flush()
            End Sub

#End Region

            Public Overrides Sub Write(ByVal buffer As Byte(), ByVal offset As Integer, ByVal count As Integer)

                Dim utf8 As New UTF8Encoding()

                ' Get the string into a stringbuilder
                Dim sbBuffer As New StringBuilder(utf8.GetString(buffer))

                Try

                    '-------------------------------------------------------------------------------'
                    '
                    ' SITE CONFIGURATION
                    ' get a reference to the site config
                    '
                    '-------------------------------------------------------------------------------'
                    Dim strHostConfigPath As String = Hosting.HostingEnvironment.MapPath(CONFIGURATION_PATH)

                    If Not File.Exists(strHostConfigPath) Then
                        'sbBuffer.Replace(HEAD_END_TAG, "<!-- Open Graph Protocol Module Error: Installation config file is missing -->")
                        mSR.Write(sbBuffer.ToString())
                        mSR.Flush()
                        Exit Sub
                    End If

                    Dim xConfigDoc As XDocument
                    Using sReader As StreamReader = File.OpenText(strHostConfigPath)
                        xConfigDoc = XDocument.Load(sReader)
                    End Using

                    Dim strCurrentDomain As String = currentApp.Request.Url.Host
                    Dim oSiteConfig As XElement = (From xconfig In xConfigDoc.Descendants("Site")
                            Where xconfig.Attribute("httpAlias").Value = strCurrentDomain
                            Select xconfig).FirstOrDefault

                    ' no need to continue if this site doesn't have a configuration saved
                    If oSiteConfig Is Nothing Then
                        'sbBuffer.Replace(HEAD_END_TAG, "<!-- Open Graph Protocol Module Error: Site domain name doesn't match in config -->")
                        mSR.Write(sbBuffer.ToString())
                        mSR.Flush()
                        Exit Sub
                    End If

                    '-------------------------------------------------------------------------------'
                    '
                    ' PAGE CONFIGURATION
                    ' get a reference to the page's Open Graph Protocol Config
                    '
                    '-------------------------------------------------------------------------------'
                    Dim strPortalId As String = oSiteConfig.Attribute("portalId").Value
                    Dim strTabId As String = Regex.Match(currentApp.Request.Url.Query, "^\?TabID=(\d+).*", RegexOptions.IgnoreCase).Groups(1).Value
                    Dim strSiteConfigPath As String = Hosting.HostingEnvironment.MapPath(String.Format(SITE_CONFIGURATION_PATH, strPortalId, strTabId))

                    If Not File.Exists(strSiteConfigPath) Then
                        'sbBuffer.Replace(HEAD_END_TAG, "<!-- Open Graph Protocol Module Error: Site Configuration File is missing -->")
                        mSR.Write(sbBuffer.ToString())
                        mSR.Flush()
                        Exit Sub
                    End If

                    Dim xSite As XDocument
                    Using sReader As StreamReader = File.OpenText(strSiteConfigPath)
                        xSite = XDocument.Load(sReader)
                    End Using

                    ' get the current URL from the other config file
                    Dim oPageConfig As XElement = (From xconfig In xSite.Descendants("ogSettings")
                            Select xconfig).FirstOrDefault

                    Dim strUrl As String = oPageConfig.Element("Url").Value

                    Dim intPortalId As Integer = Integer.Parse(strPortalId, System.Globalization.NumberStyles.Integer)
                    Dim intTabId As Integer = Integer.Parse(oPageConfig.Element("TabId").Value, System.Globalization.NumberStyles.Integer)
                    Dim ctlPage As New Entities.Tabs.TabController
                    Dim oTab As Entities.Tabs.TabInfo = ctlPage.GetTab(intTabId, intPortalId, False)

                    Dim strMlUrl As String = strUrl.Replace(String.Concat(strCurrentDomain, "/"), String.Concat(strCurrentDomain, "/", Threading.Thread.CurrentThread.CurrentCulture.Name, "/"))

                    ' only proceed if this is the page that the module is on
                    If Not String.Equals(strUrl.ToLower(), oTab.FullUrl.ToLower()) And Not String.Equals(strMlUrl.ToLower(), oTab.FullUrl.ToLower()) Then
                        'sbBuffer.Replace(HEAD_END_TAG, HEAD_END_TAG)
                        mSR.Write(sbBuffer.ToString())
                        mSR.Flush()
                        Exit Sub
                    End If

                    'Dim blnReplace As Boolean = Boolean.Parse(oElement.Attribute("OverrideOpenGraph").Value)

                    '-------------------------------------------------------------------------------'
                    '
                    ' INJECT OPEN GRAPH PROTOCOL
                    ' add the open graph protocol tags to the page header
                    '
                    '-------------------------------------------------------------------------------'
                    If Not xSite Is Nothing Then

                        Dim sbReplacement As New StringBuilder

                        ' create the Open Graph Protocol tagss
                        Dim siteElement As XElement = (From xconfig In xSite.Descendants("ogSettings")
                                Select xconfig).FirstOrDefault

                        If Not siteElement.Element(TITLE_ELEMENT) Is Nothing AndAlso Not String.IsNullOrEmpty(siteElement.Element(TITLE_ELEMENT).Value) Then
                            sbReplacement.AppendFormat(TITLE_OG_TAG_FORMAT, siteElement.Element(TITLE_ELEMENT).Value)
                        End If
                        If Not siteElement.Element(URL_ELEMENT) Is Nothing AndAlso Not String.IsNullOrEmpty(siteElement.Element(URL_ELEMENT).Value) Then
                            sbReplacement.AppendFormat(URL_OG_TAG_FORMAT, siteElement.Element(URL_ELEMENT).Value)
                        End If
                        If Not siteElement.Element(IMAGE_ELEMENT) Is Nothing AndAlso Not String.IsNullOrEmpty(siteElement.Element(IMAGE_ELEMENT).Value) Then
                            If Regex.IsMatch(siteElement.Element(IMAGE_ELEMENT).Value, IMAGE_MATCH, RegexOptions.IgnoreCase) Then

                                Dim strFileId As String = Regex.Match(siteElement.Element(IMAGE_ELEMENT).Value, IMAGE_ID_MATCH, RegexOptions.IgnoreCase).Value
                                Dim oImage As IFileInfo = FileManager.Instance().GetFile(Integer.Parse(strFileId, NumberStyles.Integer))

                                If Not oImage Is Nothing Then
                                    ' only append the file tag if the file still exists
                                    Dim strImagePath As String = String.Concat(HTTP_PREFIX, oPageConfig.Element("HttpAlias").Value, FileManager.Instance().GetUrl(oImage))
                                    sbReplacement.AppendFormat(IMAGE_OG_TAG_FORMAT, strImagePath)
                                Else
                                    ' file doesn't exist any longer 
                                    ' Need to delete the reference
                                End If

                            Else
                                sbReplacement.AppendFormat(IMAGE_OG_TAG_FORMAT, siteElement.Element(IMAGE_ELEMENT).Value)
                            End If
                        End If

                        If Not siteElement.Element(DESCRIPTION_ELEMENT) Is Nothing AndAlso Not String.IsNullOrEmpty(siteElement.Element(DESCRIPTION_ELEMENT).Value) Then
                            sbReplacement.AppendFormat(DESCRIPTION_OG_TAG_FORMAT, siteElement.Element(DESCRIPTION_ELEMENT).Value)
                        End If
                        If Not siteElement.Element(LOCALE_ELEMENT) Is Nothing AndAlso Not String.IsNullOrEmpty(siteElement.Element(LOCALE_ELEMENT).Value) Then
                            sbReplacement.AppendFormat(LOCALE_OG_TAG_FORMAT, siteElement.Element(LOCALE_ELEMENT).Value)
                        End If
                        If Not siteElement.Element(SITENAME_ELEMENT) Is Nothing AndAlso Not String.IsNullOrEmpty(siteElement.Element(SITENAME_ELEMENT).Value) Then
                            sbReplacement.AppendFormat(SITENAME_OG_TAG_FORMAT, siteElement.Element(SITENAME_ELEMENT).Value)
                        End If
                        If Not siteElement.Element(TYPE_ELEMENT) Is Nothing AndAlso Not String.IsNullOrEmpty(siteElement.Element(TYPE_ELEMENT).Value) Then
                            sbReplacement.AppendFormat(TYPE_OG_TAG_FORMAT, siteElement.Element(TYPE_ELEMENT).Value)
                        End If
                        If Not siteElement.Element(FACEBOOKADMINS_ELEMENT) Is Nothing AndAlso Not String.IsNullOrEmpty(siteElement.Element(FACEBOOKADMINS_ELEMENT).Value) Then
                            sbReplacement.AppendFormat(FACEBOOKADMINS_OG_TAG_FORMAT, siteElement.Element(FACEBOOKADMINS_ELEMENT).Value)
                        End If
                        If Not siteElement.Element(FACEBOOKAPPID_ELEMENT) Is Nothing AndAlso Not String.IsNullOrEmpty(siteElement.Element(FACEBOOKAPPID_ELEMENT).Value) Then
                            sbReplacement.AppendFormat(FACEBOOKAPPID_OG_TAG_FORMAT, siteElement.Element(FACEBOOKAPPID_ELEMENT).Value)
                        End If

                        sbReplacement.Append(HEAD_END_TAG)

                        sbBuffer.Replace(HEAD_END_TAG, sbReplacement.ToString)

                        ' Write to the stream
                        mSR.Write(sbBuffer.ToString())
                        mSR.Flush()

                    End If

                Catch ex As Exception
                    LogException(ex)

                    ' write the original content back to the stream
                    'Dim sbBuffer As New StringBuilder(utf8.GetString(buffer))
                    sbBuffer.Replace(HEAD_END_TAG, ERROR_HEAD_TAG)
                    mSR.Write(sbBuffer.ToString())
                    mSR.Flush()

                End Try

            End Sub

        End Class

    End Class

End Namespace